package yolo

import (
	"appengine"
	"appengine/memcache"
	"appengine/taskqueue"
	"appengine/urlfetch"
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
	"regexp"
	"strconv"
	"strings"
	"time"
)

const (
	chromiumBuildbot = "http://build.chromium.org/p/chromium/json"
	chromiumBuildbotSiteURL = "build.chromium.org"
	// Buildbot/builders/<builder>/builds/<buildnum> => BuildData
)

var (
	bugLineRegexp     = regexp.MustCompile("\nBUG=([0-9, ]+)")
	reviewUrlRegexp   = regexp.MustCompile("\nReview URL: ([^ ]+)")
	reviewIssueRegexp = regexp.MustCompile("http://(.*)/([0-9]+)")

	invalidTaskNameRegxp = regexp.MustCompile("[^a-zA-Z0-9]")

	ActivityBuildStart   = "BuildStart"
	ActivityBuildFailed  = "BuildFailed"
	ActivityBuildSuccess = "BuildSuccess"

	storeBuildStart   = false
	storeBuildSuccess = false
)

func parseChangeDescription(s string) (summary, bug, review string) {
	summary = s
	if n := strings.Index(s, "\n"); n > 0 {
		summary = s[:n]
	}
	if m := bugLineRegexp.FindStringSubmatch(s); m != nil {
		bug = m[1]
	}
	if m := reviewUrlRegexp.FindStringSubmatch(s); m != nil {
		review = m[1]
	}
	return
}

func getReviewIssue(review string) (siteurl string, issue int64) {
	if m := reviewIssueRegexp.FindStringSubmatch(review); m != nil {
		siteurl = m[1]
		issue, _ = strconv.ParseInt(m[2], 10, 0)
	}
	return
}

// Buildbot data

type BuilderData struct {
	Basedir       string    `json: "basedir"`
	CachedBuilds  []float64 `json: "cachedBuilds"`
	Category      string    `json: "category"`
	CurrentBuilds []float64 `json: "currentBuilds"`
	PendingBuilds float64   `json: "pendingBuilds"`
	Slaves        []string  `json: "slaves"`
	State         string    `json: "state"`
}

func (builder BuilderData) GetLatestBuild() (buildnum int64) {
	for _, build := range builder.CurrentBuilds {
		if int64(build) > buildnum {
			buildnum = int64(build)
		}
	}
	for _, build := range builder.CachedBuilds {
		if int64(build) > buildnum {
			buildnum = int64(build)
		}
	}
	return buildnum
}

func (builder BuilderData) GetLastBuild() (buildnum int64) {
	current := map[int64]bool{}
	for _, build := range builder.CurrentBuilds {
		current[int64(build)] = true
	}
	for _, build := range builder.CachedBuilds {
		if !current[int64(build)] && int64(build) > buildnum {
			buildnum = int64(build)
		}
	}
	return buildnum
}

type ProjectData struct {
	BuildbotURL string `json: "buildbotURL"`
	ProjectName string `json: "projectName"`
	ProjectURL  string `json: "projectURL"`
}

type StepData struct {
	Eta float64 `json: "eta"`
	// Expectations
	IsFinished bool          `json: "isFinished"`
	IsStarted  bool          `json: "isStarted"`
	Logs       [][]string    `json: "logs"`
	Name       string        `json: "name"`
	Results    []interface{} `json: "results"` // Results[1] is error code?
	// Statistics
	StepNumber float64   `json: "step_number"`
	Text       []string  `json: "text"`
	Times      []float64 `josn: "times"`
	// Urls
}

func (step StepData) IsSuccess() bool {
	if len(step.Results) == 0 {
		return false
	}
	if r, ok := step.Results[0].(float64); ok && r == 0 {
		return true
	}
	return false
}

type ChangeData struct {
	Branch   string   `json: "branch"`
	Comments string   `json: "comments"`
	Files    []string `json: "files"`
	Number   float64  `json: "number"`
	Revision string   `json: "revision"`
	RevLink  string   `json: "revlink"`
	When     float64  `json: "when"`
	Who      string   `json: "string"`
}

type SourceStampData struct {
	Branch   string       `json: "branch"`
	Changes  []ChangeData `json: "changes"`
	Revision string       `json: "revision"`
}

type BuildData struct {
	Blame       []string   `json: "blame"`
	BuilderName string     `json: "builderName"`
	Changes     []string   `json: "changes"`
	CurrentStep StepData   `json: "currentStep"`
	Eta         float64    `json: "eta"`
	Logs        [][]string `json: "logs"`
	Number      float64    `json: "number"`
	// Properties
	Reason string `json: "reason"`
	// Requests
	Slave       string          `json: "slave"`
	SourceStamp SourceStampData `json: "sourceStamp"`
	Steps       []StepData      `json: "step"`
	Text        []string        `json: "text"`
	Times       []float64       `json: "times"`
}

type SlaveData struct {
	// AccessURI string `json: "access_uri"`
	// Admin string `json: "admin"`
	// Builders map[string][]interface{} `json: "builders"`
	Connected     bool        `json: "connected"`
	Host          string      `json: "host"`
	Name          string      `json: "name"`
	RunningBuilds []BuildData `json: "runningBuilds"`
	Version       string      `json: "version"`
}

type BuildbotData struct {
	Builders map[string]BuilderData `json: "builders"`
	// ChangeSources	map[string]ChangeSourcesData `json: "change_sources"`
	Project ProjectData          `json: "project"`
	Slaves  map[string]SlaveData `json: "slaves"`
}

type Buildbot struct {
	url_ *url.URL
	Data BuildbotData
}

func NewBuildbot(path string) *Buildbot {
	buildbotUrl, _ := url.Parse(path)
	return &Buildbot{
		url_: buildbotUrl}
}

func (b *Buildbot) fetch(c *http.Client) (err error) {
	resp, err := c.Get(b.url_.String())
	if err != nil {
		return err
	}
	d := json.NewDecoder(resp.Body)
	err = d.Decode(&b.Data)
	return nil
}

func (b *Buildbot) fetchBuild(c *http.Client, builder string, buildnum int64, build *BuildData) (err error) {
	burl, err := b.url_.Parse(fmt.Sprintf("json/builders/%s/builds/%d", builder, buildnum))
	if err != nil {
		return err
	}
	resp, err := c.Get(burl.String())
	if err != nil {
		return err
	}
	d := json.NewDecoder(resp.Body)
	err = d.Decode(build)
	return nil
}

func getBuildbotActivityData(w http.ResponseWriter, r *http.Request) {
	c := appengine.NewContext(r)
	builder := r.FormValue("builder")
	buildnum, err := strconv.ParseInt(r.FormValue("buildnum"), 10, 0)
	if err != nil {
		buildnum = -1
	}
	activities, err := checkBuildbot(c, builder, buildnum, false, false)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	a, err := json.Marshal(activities)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	w.Header().Set("Content-Type", "application/json")
	w.Write(a)
}

func postStartCheckBuildbot(w http.ResponseWriter, r *http.Request) {
	c := appengine.NewContext(r)

	_, err := postTask(c, "/worker/buildbot_start_check", "default",
		PostParams{})
	if err != nil {
		c.Warningf("post err:%s", err)
		http.Error(w, fmt.Sprintf("Posting buildbot_start_check returned %s", err.Error()), http.StatusInternalServerError)
	}
	fmt.Fprintf(w, "Posted buildbot_start_check.")
}

func startCheckBuildbot(w http.ResponseWriter, r *http.Request) {
	c := appengine.NewContext(r)

	builder := r.FormValue("builder")
	buildnum, err := strconv.ParseInt(r.FormValue("buildnum"), 10, 0)
	if err != nil {
		buildnum = -1
	}
	update := r.FormValue("update") != ""
	activities, err := checkBuildbot(c, builder, buildnum, update, true)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}

	err = RecordActivityDataMulti(c, activities)
	if err != nil {
		c.Warningf("%s: Storing activities failed: %s", currentLocation(), err.Error())
	}

	// fetch latest version in future
	if builder == "" {
		t := taskqueue.NewPOSTTask("/worker/buildbot_start_check",
			map[string][]string{})
		t.Name = fmt.Sprintf("check-buidbot-%s", (time.Now().Format("2006-01-02T15-04")))
		t.Delay = 60 * 1000 * 1000 // 1 min
		_, err = taskqueue.Add(c, t, "default")
		if err != nil {
			c.Warningf("post err:%s", err)
		}
	}
}

func checkBuildbot(c appengine.Context, builder string, buildnum int64, update bool, scan bool) (activities []*ActivityData, err error) {
	c.Debugf("check: %s %s %d", chromiumBuildbot, builder, buildnum)
	buildbot := NewBuildbot(chromiumBuildbot)
	if builder != "" {
		var build BuildData
		err = buildbot.fetchBuild(urlfetch.Client(c), builder, buildnum, &build)
		if err != nil {
			c.Warningf("fetchBuild %s %d failed: %s", builder, buildnum, err)
			return
		}
		activities = append(activities, putBuildDataActivity(c, buildbot, build)...)
		if len(build.Times) > 1 && build.Times[1] > 0 {
			putBuildData(c, buildbot, build)
		}
		if !scan {
			return activities, nil
		}
		for checkBuildbotBuilder(c, buildbot, builder, buildnum-1) {
			if !update {
				break
			}
			if buildnum <= 0 {
				break
			}
			buildnum = buildnum - 1
		}
		return activities, nil
	}
	err = buildbot.fetch(urlfetch.Client(c))
	if err != nil {
		c.Warningf("fetch failed: %s", err)
		return
	}
	for builderName, builderData := range buildbot.Data.Builders {
		c.Debugf("builder: %s\n", builderName)
		bnum := builderData.GetLastBuild()
		markBuildNum(c, buildbot, builderName, bnum-1)
		if !scan {
			continue
		}
		for checkBuildbotBuilder(c, buildbot, builderName, bnum) {
			if !update {
				break
			}
			if bnum <= 0 {
				break
			}
			bnum = bnum - 1
		}
	}
	for slaveName, slaveData := range buildbot.Data.Slaves {
		c.Debugf("slave: %s\n", slaveName)
		if !slaveData.Connected {
			continue
		}
		for _, build := range slaveData.RunningBuilds {
			activities = append(activities, putBuildDataActivity(c, buildbot, build)...)
		}
	}
	return activities, nil
}

func buildDataMemcacheKey(buildbot *Buildbot, builder string, buildnum int64) string {
	return fmt.Sprintf("buildbot:%s:%s:%d", buildbot.url_, builder, buildnum)
}

func putBuildData(c appengine.Context, buildbot *Buildbot, build BuildData) {
	key := buildDataMemcacheKey(buildbot, build.BuilderName, int64(build.Number))
	item := &memcache.Item{Key: key, Object: build}
	err := memcache.JSON.Set(c, item)
	if err != nil {
		c.Warningf("memcache store %s error: %s", key, err)
	}
	c.Infof("memcache store %s", key)
}

func markBuildNum(c appengine.Context, buildbot *Buildbot, builder string, buildnum int64) {
	key := buildDataMemcacheKey(buildbot, builder, buildnum)
	var build BuildData
	_, err := memcache.JSON.Get(c, key, &build)
	if err != nil {
		build.BuilderName = builder
		build.Number = float64(buildnum)
		item := &memcache.Item{Key: key, Object: build}
		err = memcache.JSON.Set(c, item)
		if err != nil {
			c.Warningf("memcache set %s error: %s", key, err)
		}
		c.Infof("memcache mark %s", key)
	}
}

func createTaskName(builder string, buildnum int64) string {
	s := fmt.Sprintf("builder-%s-builds-%d", builder, buildnum)
	return invalidTaskNameRegxp.ReplaceAllString(s, "-")
}

func checkBuildbotBuilder(c appengine.Context, buildbot *Buildbot, builder string, buildnum int64) (found bool) {
	key := buildDataMemcacheKey(buildbot, builder, buildnum)
	var build BuildData
	_, err := memcache.JSON.Get(c, key, &build)
	if err == nil {
		c.Debugf("memcache get %s found", key)
		return true
	}
	c.Infof("check builder %s buildnum %d", builder, buildnum)
	t := taskqueue.NewPOSTTask("/worker/buildbot_start_check",
		map[string][]string{"builder": {builder},
		"buildnum": {strconv.FormatInt(buildnum, 10)}})
	t.Name = createTaskName(builder, buildnum)
	_, err = taskqueue.Add(c, t, "modest")
	if err != nil {
		c.Warningf("post %s err:%s", t.Name, err)
	}
	return false
}

func putBuildDataActivity(c appengine.Context, buildbot *Buildbot, build BuildData) (activities []*ActivityData) {
	buildURL, _ := buildbot.url_.Parse(fmt.Sprintf("builders/%s/builds/%d", build.BuilderName, int(build.Number)))
	c.Debugf("buildURL: %s", buildURL)

	files := []string{}
	recipients := []string{}

	for _, change := range build.SourceStamp.Changes {
		/*
		_, _, review := parseChangeDescription(change.Comments)
		siteurl, issue := getReviewIssue(review)
		if issue > 0 {
			issues = append(issues, CreateIssueId(siteurl, issue))
			issueOwners = append(issueOwners, change.Who)
		}
		*/
		recipients = append(recipients, change.Who)
		files = append(files, change.Files...)
	}

	newActivityData := func () *ActivityData {
		a := new(ActivityData)
		a.SiteURL = chromiumBuildbotSiteURL
		a.Recipients = recipients
		return a
	}

	if storeBuildStart {
		a := newActivityData()
		a.Activity.User = build.BuilderName
		a.Activity.Time = time.Unix(int64(build.Times[0]), 0)
		a.Activity.URL = buildURL.String()
		a.Activity.Snippet = fmt.Sprintf("build start @ %s", build.SourceStamp.Revision)
		a.Activity.Type = ActivityBuildStart
		activities = append(activities, a)
	}

	buildSuccess := true
	for _, step := range build.Steps {
		if step.IsFinished && !step.IsSuccess() {
			a := newActivityData()
			a.Activity.SiteURL = chromiumBuildbotSiteURL
			a.Activity.User = build.BuilderName
			a.Activity.Time = time.Unix(int64(step.Times[1]), 0)
			stepURL, _ := buildbot.url_.Parse(fmt.Sprintf("builders/%s/builds/%d/steps/%s/", build.BuilderName, int(build.Number), step.Name))
			a.Activity.URL = stepURL.String()
			a.Activity.Snippet = fmt.Sprintf("build failed %s: %v", step.Name, step.Text)
			a.Activity.Type = ActivityBuildFailed
			a.ActivityFiles.Files = files
			buildSuccess = false
			activities = append(activities, a)
		}
	}
	if storeBuildSuccess && len(build.Times) > 1 && build.Times[1] > 0 && buildSuccess {
		a := newActivityData()
		a.Activity.User = build.BuilderName
		a.Activity.Time = time.Unix(int64(build.Times[1]), 0)
		a.Activity.URL = buildURL.String()
		a.Activity.Snippet = "build success"
		a.Activity.Type = ActivityBuildSuccess
		activities = append(activities, a)
	}
	return activities
}
